#!/bin/bash
###############################################
#########自动部署shell实现代码#################
###############################################

# Node List 服务器节点
PRE_LIST='120.77.67.188' #"112.74.48.75"        # 预生产节点

GROUP1_LIST=""
GROUP2_LIST=""
ROLLBACK_LIST="120.77.67.188"

# 日志日期和时间变量
LOG_DATE='date "+%Y-%m-%d"' # 如果执行的话后面执行的时间，此时间是不固定的，这是记录日志使用的时间
LOG_TIME='date "+%H-%M-%S"'

# 代码打包时间变量
CDATE=$(date "+%Y-%m-%d") # 脚本一旦执行就会取一个固定时间赋值给变量，此时间是固定的
CTIME=$(date +"%H-%M-%S")

# shell env 脚本位置等变量
SHELL_NAME="deploy.sh"    # 脚本名称
SHELL_DIR="/home/hjs/deploy/"  # 脚本路径
SHELL_LOG="${SHELL_DIR}/${SHELL_NAME}.log" # 脚本执行日志文件路径

# code env 项目相关代码变量
PRO_NAME="meinv"    # 项目名称的函数
GIT_URL='https://gitee.com/hjsiamcer/meinv.git';

CODE_DIR="/deploy/code/"${PRO_NAME}    # 从版本管理系统更新的代码目录
CONFIG_DIR="/deploy/config/"${PRO_NAME}    # 保存不同项目的配置文件，一个目录里面就是一个项目的一个配置文件或多个配置文件

TMP_DIR="/deploy/tmp"            # 临时目录
TAR_DIR="/deploy/tar"            # 打包目录
LOCK_FILE="/tmp/deploy.lock" # 锁文件路径

usage(){ # 使用帮助函数
    echo $"Usage: $0 [ init | pro_init | deploy | rollback [ list | emergency | version ]"
}

init(){ #部署服务器初始化

    echo '初始化部署节点'
    writelog '初始化部署节点(init)'

    user=$(env|grep USER|cut -d "=" -f 2)

    if [ "$user" != "root" ];then
        echo '请使用root用户初始化部署服务器！'
        exit 1;
    fi 

    if [ ! -f "/deploy/deploy.lock" ];then

        mkdir -p /deploy/code/ #项目git源码
        mkdir -p /deploy/config/ #项目配置文件

        mkdir /deploy/tmp/  #项目完整的干净的源码
        mkdir /deploy/tar/  #项目全部版本的压缩包


        touch /deploy/deploy.lock  #创建锁问价

        chown -R hjs.hjs /deploy


    fi
}

pro_init(){ #项目初始化
   #项目名
   #项目git仓库地址
   mkdir -p /deploy/code/${PRO_NAME}/
   mkdir -p /deploy/config/${PRO_NAME}/base/
   mkdir -p /deploy/config/${PRO_NAME}/other/

   cd /deploy/code/${PRO_NAME} && git clone  ${GIT_URL} .

}


writelog(){ # 写入日志的函数
    LOGINFO=$1 # 将参数作为日志输入
    echo "${CDATE} ${CTIME} : ${SEHLL_NAME} : ${LOGINFO}" >> ${SHELL_LOG}
}

# 锁函数
shell_lock(){
    touch ${LOCK_FILE}

}

# 解锁函数
shell_unlock(){
    rm -f ${LOCK_FILE}
}

# 获取代码的函数
code_get(){
    echo "code_get"

    writelog 'code_get' #写入日志
    
    cd $CODE_DIR && git pull # 进入到代码目录更新代码，此处必须免密码更新，此目录仅用于代码更新不能放其他任何文件
}

code_build(){ # 代码编译函数
    echo 'code_build'
    writelog '编译(code build)'
}

code_config(){ # 将配置文件放到源代码中，然后将当前版本代码放到tmp目录并且重命名
    writelog "code_config"
    #/bin/cp -rf ${CONFIG_DIR}/base/* ${TMP_DIR}/"${PRO_NAME}" # 将配置文件放在本机保存配置文件的临时目录，用于暂时保存代码项目。之所以使用/bin/cp不使用cp是为了强制复制

    cd $CODE_DIR
    cp -rf ${CODE_DIR} ${TMP_DIR}/ # 将代码放到临时保存目录，包名为时间+版本号，准备复制到web服务器

    API_VERL=$(git show | grep commit | cut -d ' ' -f2)
    API_VER=$(echo ${API_VERL:0:8})        #版本号。  使用从仓库commit id的前8位作为版本号


    PKG_NAME="${PRO_NAME}"_"$API_VER"_"${CDATE}-${CTIME}"    # 定义代码目录名称

    cd ${TMP_DIR} && mv ${PRO_NAME} ${PKG_NAME}    #切换到临时目录， 重命名代码文件为web-demo_123-20170629-11-19-10格式
    
}

code_tar(){ # 对代码打包函数
    writelog '代码打包(code_tar)'
    cd ${TMP_DIR} && tar czf ${PKG_NAME}.tar.gz ${PKG_NAME} --exclude=".git" # 将目录打包成压缩文件,便于网络传输，并且排除.git目录


    PKG_NAME="${PRO_NAME}"_"$API_VER"_"${CDATE}-${CTIME}"    # 定义代码目录名称

    cd ${TMP_DIR} && mv ${PRO_NAME} ${PKG_NAME}    #切换到临时目录， 重命名代码文件为web-demo_123-20170629-11-19-10格式

    
}


code_scp(){ # 代码压缩包scp到客户端的函数
    writelog  "代码发布到客户端节点(code_scp)"

    for node in $PRE_LIST;do # 循环预生产服务器节点列表
        scp ${TMP_DIR}/${PKG_NAME}.tar.gz $node:/opt/webroot/ # 将压缩后的代码包复制到web服务器的/opt/webroot
    done

    for node in $GROUP1_LIST;do # 循环生产服务器节点列表
        scp ${TMP_DIR}/${PKG_NAME}.tar.gz $node:/opt/webroot/ # 将压缩后的代码包复制到web服务器的/opt/webroot
    done
}

cluster_node_add(){ #将web服务器添加至集群
    echo '将web服务器添加至集群'
    writelog "将web服务器添加至集群(cluster_node_add)"
}

cluster_node_remove(){ # 将web服务器从集群移除函数(正在部署的时候应该不处理业务)
    echo '将web服务器从集群移除: ' ${1}
    writelog " 将web服务器从集群移除(cluster_node_remove):${1}"
}

url_test(){
    URL=$1
    curl -s --head $URL |grep '200 OK'
    if [ $? -ne 0 ];then
        shell_unlock;
        writelog "test error" && exit;
    fi
}

pre_deploy(){ # 代码解压部署函数,预生产节点


    writelog "预生产节点部署(pre_deploy)"
    for node in ${PRE_LIST};do # 循环预生产服务器节点列表
       
        cluster_node_remove  ${node} # 部署之前将节点从前端负载删除

        echo  "预生产节点部署：pre_deploy, cluster_node_remove ${node}"
        ssh ${node} "cd /opt/webroot && tar zxf ${PKG_NAME}.tar.gz" #分别到web服务器执行压缩包解压命令

        
	#删除旧的软连接，并创建新的软连接指向新解压出来的代码:/webroot/web/demo-->/opt/webroot/web-demo_xxxx
        ssh  ${node} "rm -f /webroot/${PRO_NAME} && ln -s /opt/webroot/${PKG_NAME} /webroot/${PRO_NAME}" # 整个自动化的核心，创建软连接。
    done

}

pre_test(){ # 预生产主机测试函数
    for node in ${PRE_LIST};do # 循环预生产主机列表
        curl -s --head http://${node}:9999/index.html | grep "200 OK" # 测试web界面访问
            if [ $? -eq 0 ];then  # 如果访问成功
                writelog " ${node} Web Test OK!" # 记录日志
                echo " ${node} Web Test OK!"
                cluster_node_add ${node} # 测试成功之后调用添加函数把服务器添加至节点,
                writelog "pre,${node} add to cluster OK!" # 记录添加服务器到集群的日志
            else # 如果访问失败
                writelog "${node} test no OK" # 记录日志
                echo "${node} test not OK"
                shell_unlock # 调用删除锁文件函数
            break # 结束部署
        fi
    done

}

group1_deploy(){ # 代码解压部署函数.集群1
    writelog "group1_code_deploy"
    for node in ${GROUP1_LIST};do # 循环生产服务器节点列表
        cluster_node_remove $node  
        echo "group1, cluster_node_remove $node"
        ssh ${node} "cd /opt/webroot && tar zxf ${PKG_NAME}.tar.gz" # 分别到各web服务器节点执行压缩包解压命令
        ssh ${node} "rm -f /webroot/${PRO_NAME} && ln -s /opt/webroot/${PKG_NAME} /webroot/${PRO_NAME}" # 整个自动化的核心，创建软连接
    done
#    scp ${CONFIG_DIR}/other/192.168.3.13.server.xml 192.168.3.13:/webroot/web-demo/server.xml  # 将差异项目的配置文件scp到此web服务器并以项目结尾
}    

group1_test(){ # 生产主机测试函数。集群1
    for node in ${PRE_LIST};do # 循环生产主机列表
        curl -s --head http://${node}:9999/index.html | grep "200 OK" #测试web界面访问
        if [ $? -eq 0 ];then  #如果访问成功
            writelog " ${node} Web Test OK!" #记录日志
            echo "group1_test,${node} Web Test OK!"
            cluster_node_add
            writelog " ${node} add to cluster OK!" #记录将服务器 添加至集群的日志
        else #如果访问失败
            writelog "${node} test no OK" #记录日志
            echo "${node} test no OK"
            shell_unlock # 调用删除锁文件函数
            break # 结束部署
        fi
    done
}

emergency_code_get(){ #获取代码的函数
    writelog "code_get"
    cd ${CODE_DIR} && git reset --hard HEAD^   #进入到代码目录更新代码，此处必须免密码更新，此目录仅用于代码更新不能放其他任何文件
    /bin/cp -rf ${CODE_DIR} ${TMP_DIR}/ #临时保存代码并重命名，包名为时间+版本号，准备复制到web服务器
    API_VERL=$(git show | grep commit | cut -d ' ' -f2)
    API_VER=$(echo ${API_VERL:0:8}) #取八位
}

emergency(){  #紧急回退到上一个版本函数
        echo "紧急回退(emergency)"
       
        shell_lock; # 执行部署之前创建锁。如果同时有其他人执行则提示锁文件存在
        emergency_code_get; # 获取代码
        code_build; # 如果要编译执行编译函数
        code_config; # cp配置文件
        code_tar;    # 打包
        code_scp;    # scp到服务器
        pre_deploy;  # 预生产环境部署
        #pre_test;    # 预生产环境测试
        group1_deploy; # 生产环境部署
       # group1_test;   # 生产环境测试
        shell_unlock; # 执行完成后删除锁文件
}


rollback_fun(){
    for node in $ROLLBACK_LIST;do # 循环服务器节点列表
        # 注意一定要加"号，否则无法在远程执行命令
        ssh $node "rm -f /webroot/${PRO_NAME} && ln -s /opt/webroot/$1 /webroot/${PRO_NAME}" # 立即回滚到指定的版本，$1即指定的版本参数
        echo "${node} rollback success!"
        done
}

rollback(){ # 代码回滚主函数  如果回滚参数是list就可以查看当前可以回滚的版本
    if [ -z $1 ];then
        shell_unlock # 删除锁文件
        echo "请输入需要回滚的版本号" && exit 3;
    fi
    case $1 in # 把第二个参数做当自己的第一个参数 
        list) #从这里可以看到所有的版本，这些版本在每个节点的/opt/webroot目录也存在,但是这也不能绝对保证
            ls -l /deploy/tmp/*.tar.gz | grep ${PRO_NAME}
            ;;
        *)
            rollback_fun $1
    esac
            
}


# 主函数
main(){    
    if [ -f $LOCK_FILE ];then    # 先判断锁文件在不在
        echo "Deploy is running" && exit 10;     # 如果有锁文件直接退出
    fi 
    DEPLOY_METHOD=$1    # 避免出错误将脚本的第一个参数作为变量
    ROLLBACK_VER=$2

    case $DEPLOY_METHOD in
        init)
            init; #部署服务器初始化
	    ;;
        pro_init)
            pro_init; #项目初始化
            ;;
        deploy)        # 如果第一个参数是deploy就执行以下操作
        shell_lock; # 执行部署之前创建锁。如果同时有其他人执行则提示锁文件存在
        code_get;
        code_build; # 如果要编译执行编译函数
        code_config; # cp配置文件
        code_tar;    # 打包
        code_scp;    # scp到服务器
        pre_deploy;  # 预生产环境部署
        #pre_test;    # 预生产环境测试
        group1_deploy; # 生产环境部署
       # group1_test;   # 生产环境测试
        shell_unlock; # 执行完成后删除锁文件

            ;;
        rollback)    # 如果第一个参数是rollback就执行以下操作
            shell_lock;    # 回滚之前也是先创建锁文件
            rollback  $ROLLBACK_VER;    # 执行完成删除锁文件
            shell_unlock;
            ;;
        *)    # 其他输入执行以下操作
            usage;
    esac
}
# 执行主函数并把第一个变量当参数
main $1 $2 
